﻿using DotNetExtensions.AspNet.Helpers;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace DotNetExtensions.AspNet.IOC
{

    public class RegistTypeItem
    {
        public RegistTypeItem() { }
        public RegistTypeItem(Type implType, Type type, InjectKind kind, InjectScope scope, string named)
        {
            ImplType = implType;
            Type = type;
            Kind = kind;
            Scope = scope;

            Name = named ?? implType?.FullName;
        }

        public string Name { get; set; }
        public Type ImplType { get; set; }
        public Type Type { get; set; }
        public InjectKind Kind { get; set; }
        public InjectScope Scope { get; set; }
    }

    public class ContainerBuilder : Singleton<ContainerBuilder>
    {
        private readonly ConcurrentDictionary<Type, object> resolvedTypes = new ConcurrentDictionary<Type, object>();
        private readonly ConcurrentBag<RegistTypeItem> registeredTypeItems = new ConcurrentBag<RegistTypeItem>();
        public void RegisterType(Type implType, Type type, InjectKind kind, InjectScope scope,string named)
        {
            registeredTypeItems.Add(new RegistTypeItem(implType, type, kind, scope, named));
        }
        public bool IsRegistered(string typeFullName)
        {
            return registeredTypeItems.Any(c => c.Type.FullName == typeFullName);
        }
        public bool IsRegistered(string typeFullName, out Type type)
        {
            type = null;
            var registeredItem = registeredTypeItems.Where(c => c.Type.FullName == typeFullName).FirstOrDefault();
            if (registeredItem == null) return false;
            type = registeredItem.Type;
            return true;
        }
        public bool IsRegistered(Type type)
        {
            return registeredTypeItems.Any(c => c.Type == type);
        }
        public bool IsRegistered(Type type, InjectKind kind)
        {
            return registeredTypeItems.Any(c => c.Type == type && c.Kind == kind);
        }
        public bool IsRegisteredWithName(string name, Type type)
        {
            return registeredTypeItems.Any(c => c.Type == type && c.Name == name);
        }
        public object Resolve(string typeFullName)
        {
            if (IsRegistered(typeFullName, out Type type))
            {
                return Resolve(type);
            }
            return null;
        }
        public T Resolve<T>()
        {
            lock (mutex)
            {
                object instance = Resolve(typeof(T));
                return (T)instance;
            }
        }

        public object Resolve(Type type)
        {
            lock (mutex)
            {
                object instance = default(object);

                try
                {
                    if (!IsRegistered(type, InjectKind.Typed)) throw new Exception(string.Format("Type '{0}' unregistered", type.FullName));

                    RegistTypeItem registTypeItem = registeredTypeItems.FirstOrDefault(c => c.Type == type && c.Kind == InjectKind.Typed);

                    var implType = registTypeItem.ImplType;

                    var constructs = implType.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
                    if (constructs == null || constructs.Length == 0) throw new Exception(string.Format("Type '{0}' has no public constructor", implType.FullName));
                    // use first public constructor
                    ConstructorInfo constructorInfo = constructs[0];

                    switch (registTypeItem.Scope)
                    {
                        case InjectScope.Instance:
                            instance = Resovle(implType, constructorInfo);
                            break;
                        case InjectScope.Scoped:
                        case InjectScope.Single:
                            if (!resolvedTypes.TryGetValue(type, out instance)) instance = Resovle(implType, constructorInfo);
                            break;
                        default:
                            break;
                    }

                    resolvedTypes.TryUpdate(type, instance);
                }
                catch (Exception)
                {
                    throw;
                }

                return instance;
            }
        }

        public object ResolveNamed(string name, Type type)
        {
            lock (mutex)
            {
                object instance = default(object);

                try
                {
                    if (!IsRegisteredWithName(name, type)) throw new Exception(string.Format("Named '{0}' type '{1}' unregistered",name, type.FullName));

                    RegistTypeItem registTypeItem = registeredTypeItems.FirstOrDefault(c => c.Type == type && c.Name == name);
                    var implType = registTypeItem.ImplType;
                    var constructs = implType.GetConstructors(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
                    if (constructs == null || constructs.Length == 0) throw new Exception(string.Format("Type '{0}' has no public constructor", implType.FullName));
                    // use first public constructor
                    ConstructorInfo constructorInfo = constructs[0];

                    switch (registTypeItem.Scope)
                    {
                        case InjectScope.Instance:
                            instance = Resovle(implType, constructorInfo);
                            break;
                        case InjectScope.Scoped:
                        case InjectScope.Single:
                            if (!resolvedTypes.TryGetValue(implType, out instance)) instance = Resovle(implType, constructorInfo);
                            break;
                        default:
                            break;
                    }

                    resolvedTypes.TryUpdate(implType, instance);
                }
                catch (Exception)
                {
                    throw;
                }

                return instance;
            }
        }
        public T ResolveNamed<T>(string name)
        {
            lock (mutex)
            {
                object instance = ResolveNamed(name, typeof(T));
                return (T)instance;
            }
        }


        public object Resolve(Type type, Type[] parameters)
        {
            lock (mutex)
            {
                object instance = default;

                try
                {
                    if (!IsRegistered(type, InjectKind.Typed)) throw new Exception(string.Format("Type '{0}' unregistered", type.FullName));

                    RegistTypeItem registTypeItem = registeredTypeItems.FirstOrDefault(c => c.Type == type && c.Kind == InjectKind.Typed);
                    var implType = registTypeItem.ImplType;
                    var constructorInfo = implType.GetConstructor(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null);
                    if (constructorInfo == null) throw new Exception(string.Format("Type '{0}' did not find the specified public constructor", implType.FullName));

                    switch (registTypeItem.Scope)
                    {
                        case InjectScope.Instance:
                            instance = Resovle(implType, constructorInfo);
                            break;
                        case InjectScope.Scoped:
                        case InjectScope.Single:
                            if (!resolvedTypes.TryGetValue(type, out instance)) instance = Resovle(implType, constructorInfo);
                            break;
                        default:
                            break;
                    }

                    resolvedTypes.TryUpdate(type, instance);
                }
                catch (Exception)
                {
                    throw;
                }

                return instance;
            }
        }

        public object ResolveNamed(string name, Type type, Type[] parameters)
        {
            lock (mutex)
            {
                object instance = default;

                try
                {
                    if (!IsRegisteredWithName(name, type)) throw new Exception(string.Format("Named '{0}' type '{1}' unregistered", name, type.FullName));

                    RegistTypeItem registTypeItem = registeredTypeItems.FirstOrDefault(c => c.Type == type && c.Name == name);
                    var implType = registTypeItem.ImplType;
                    var constructorInfo = implType.GetConstructor(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null);
                    if (constructorInfo == null) throw new Exception(string.Format("Type '{0}' did not find the specified public constructor", implType.FullName));

                    switch (registTypeItem.Scope)
                    {
                        case InjectScope.Instance:
                            instance = Resovle(implType, constructorInfo);
                            break;
                        case InjectScope.Scoped:
                        case InjectScope.Single:
                            if (!resolvedTypes.TryGetValue(implType, out instance)) instance = Resovle(implType, constructorInfo);
                            break;
                        default:
                            break;
                    }

                    resolvedTypes.TryUpdate(implType, instance);
                }
                catch (Exception)
                {
                    throw;
                }

                return instance;
            }
        }

        private object Resovle(Type type, ConstructorInfo constructorInfo)
        {
            ParameterInfo[] parameterInfos = constructorInfo.GetParameters();

            if (parameterInfos == null || parameterInfos.Length == 0) return constructorInfo.Invoke(null);

            // resovle construct parameters
            int length = parameterInfos.Length;
            Type[] parameterTypes = new Type[length];
            object[] parameterValues = new object[length];

            foreach (var item in parameterInfos)
            {
                parameterTypes[item.Position] = item.ParameterType;

                object value = null;

                if (resolvedTypes.ContainsKey(item.ParameterType))
                {
                    value = Resolve(item.ParameterType);
                }
                else if (item.DefaultValue != null)
                {
                    value = item.DefaultValue;
                }
                else if (TypeHelper.IsBasicType(item.ParameterType))
                {
                    value = TypeHelper.DefaultValue(item.ParameterType);
                }
                else
                {
                    throw new Exception(string.Format("Type '{0}' constructor parameter '{1}' not implements", type.FullName, item.ParameterType.FullName));
                }

                parameterValues[item.Position] = value;
            }

            return constructorInfo.Invoke(parameterValues);
        }

        public T Resolve<T>(Type[] parameters)
        {
            lock (mutex)
            {
                object instance = Resolve(typeof(T), parameters);
                return (T)instance;
            }
        }
    }
}
